package com.tangcheng.business.sensitive;

import com.google.common.collect.Lists;
import com.tangcheng.business.sensitive.entity.exportdo.SensitiveFieldUsageInControllerDO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.ApplicationContext;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;

/**
 * 获取敏感字段在Controlle层接口的usages
 */
@Component
@Slf4j
public class SearchSensitiveFieldController implements CommandLineRunner {

    @Autowired
    private ApplicationContext applicationContext;
    private static Map<String, List<String>> sensitiveFieldsMapper;
    private List<SensitiveFieldUsageInControllerDO> usages;

    @GetMapping("/export/controller/usages")
    public void exportUsagesInController(ServletResponse response) throws IOException {
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        DataMaskCommonUtils.exportExcel(usages, "user敏感字段影响接口", "user-sensitive-field-usage-controller", httpServletResponse.getOutputStream(), SensitiveFieldUsageInControllerDO.class);
    }

    private void initUtil() {
        usages = new ArrayList<>();

        sensitiveFieldsMapper = new HashMap<>();
        sensitiveFieldsMapper.put("users", Lists.newArrayList("contact_mobile", "wechat"));

    }

    @Override
    public void run(String... args) throws Exception {
//        try{
//            initUtil();
//            log.info("[searchSensitiveFields] begin=============================================");
//            Map<String, Object> controllers = applicationContext.getBeansWithAnnotation(RestController.class);
//            log.info("[allControllers]:{}",controllers.keySet());
//            for (Map.Entry<String, List<String>> stringListEntry : sensitiveFieldsMapper.entrySet()) {
//                for (String targetField : stringListEntry.getValue()) {
//                    List<String> targetPaths = findField(stringListEntry.getKey(),DataMaskCommonUtils.humpConv(targetField),controllers.entrySet());
//                    log.info("[sensitive field]"+targetField+" is used in :{}",targetPaths);
//                }
//            }
//            log.info("[searchSensitiveFields] finished==========================================");
//        }catch (Exception e){
//            log.warn("[searchSensitiveFields] failed.error:",e);
//        }
    }

    private List<String> findField(String tableName, String targetFieldName, Set<Map.Entry<String, Object>> controllers) throws ClassNotFoundException {
        List<String> paths = new ArrayList<>();
        for (Map.Entry<String, Object> controller : controllers) {
            log.info("[inController] {}", controller.getValue().getClass().getSimpleName());
            for (Method declaredMethod : controller.getValue().getClass().getDeclaredMethods()) {
                log.info("[inMethod] {}", declaredMethod.getName());
                if (handleParams(declaredMethod, targetFieldName) || handleReturn(declaredMethod, targetFieldName)) {
                    String s = addPath(declaredMethod, paths, AnnotationUtils.findAnnotation(controller.getValue().getClass(), RequestMapping.class));
                    SensitiveFieldUsageInControllerDO sensitiveFieldUsageInControllerDO = new SensitiveFieldUsageInControllerDO()
                            .setTableName(tableName)
                            .setSensitiveFieldName(targetFieldName)
                            .setControllerName(controller.getKey())
                            .setMethodName(declaredMethod.getName())
                            .setAccessPath(s);
                    usages.add(sensitiveFieldUsageInControllerDO);
                }
            }
        }
        return paths;
    }

    /**
     * @param declaredMethod
     * @param targetFieldName
     * @return
     * @throws ClassNotFoundException
     */
    private boolean handleParams(Method declaredMethod, String targetFieldName) throws ClassNotFoundException {
        Parameter[] parameters = declaredMethod.getParameters();
        for (Parameter parameter : parameters) {
            if (parameter.getName().contains(targetFieldName)) {
                return true;
            }
            Set<String> s = new HashSet<>();
            boolean B = handleField(parameter.getType(), targetFieldName, s);
            if (Boolean.TRUE.equals(B)) {
                return true;
            }
            Type type = parameter.getParameterizedType();
            if (type instanceof ParameterizedType) {
                ParameterizedType parameterizedType = (ParameterizedType) type;
                //获取泛型列表
                Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
                for (Type actualTypeArgument : actualTypeArguments) {
                    Set<String> visitedClasses = new HashSet<>();
                    boolean b = handleField(actualTypeArgument, targetFieldName, visitedClasses);
                    if (Boolean.TRUE.equals(b)) {
                        return true;
                    }
                }
            }
        }
        return false;
    }

    private boolean handleReturn(Method declaredMethod, String targetFieldName) throws ClassNotFoundException {
        Set<String> s = new HashSet<>();
        boolean B = handleField(declaredMethod.getReturnType(), targetFieldName, s);
        if (Boolean.TRUE.equals(B)) {
            return true;
        }
        Type type = declaredMethod.getGenericReturnType();
        if (type instanceof ParameterizedType) {
            ParameterizedType genericReturnType = (ParameterizedType) type;
            Type[] actualTypeArguments = genericReturnType.getActualTypeArguments();
            for (Type actualTypeArgument : actualTypeArguments) {
                Set<String> visitedClasses = new HashSet<>();
                boolean b = handleField(actualTypeArgument, targetFieldName, visitedClasses);
                if (Boolean.TRUE.equals(b)) {
                    return true;
                }
            }
        }
        return false;
    }

    private boolean handleField(Type type, String targetFieldName, Set<String> visitedClasses) {
        if (type.getTypeName().startsWith("com.tangcheng")) {
            if (visitedClasses.contains(type.getTypeName())) {
                return false;
            }
            log.info("[search] in class:{}", type.getTypeName());
            visitedClasses.add(type.getTypeName());
            List<Field> fields = new ArrayList<>();
            Class tempClass = (Class) type;
            while (Objects.nonNull(tempClass)) {
                fields.addAll(Arrays.asList(tempClass.getDeclaredFields()));
                tempClass = tempClass.getSuperclass();
            }
            for (Field field : fields) {
                if (field.getName().contains(targetFieldName)) {
                    return true;
                }
                boolean B = handleField(field.getType(), targetFieldName, visitedClasses);
                if (Boolean.TRUE.equals(B)) {
                    return true;
                }
                Type t = field.getGenericType();
                if (t instanceof ParameterizedType) {
                    ParameterizedType genericType = (ParameterizedType) t;
                    Type[] actualTypeArguments = genericType.getActualTypeArguments();
                    for (Type actualTypeArgument : actualTypeArguments) {
                        boolean b = handleField(actualTypeArgument, targetFieldName, visitedClasses);
                        if (Boolean.TRUE.equals(b)) {
                            return true;
                        }
                    }
                }
            }
        } else {
            if (type instanceof ParameterizedType) {
                ParameterizedType t = (ParameterizedType) type;
                Type[] actualTypeArguments = t.getActualTypeArguments();
                for (Type actualTypeArgument : actualTypeArguments) {
                    boolean b = handleField(actualTypeArgument, targetFieldName, visitedClasses);
                    if (Boolean.TRUE.equals(b)) {
                        return true;
                    }
                }
            }
        }
        return false;
    }


    private String addPath(Method method, List<String> paths, RequestMapping requestMapping) {
        String prefix = Optional.ofNullable(requestMapping)
                .map(item -> item.value())
                .filter(item -> item.length > 0)
                //可能没有指定前缀
                .map(item -> item[0])
                .orElse("");
        String path = prefix;
        PostMapping postMappingAnnocation = AnnotationUtils.findAnnotation(method, PostMapping.class);
        if (Objects.nonNull(postMappingAnnocation)) {
            path += postMappingAnnocation.value()[0];
        }
        GetMapping getMappingAnnotation = AnnotationUtils.findAnnotation(method, GetMapping.class);
        if (Objects.nonNull(getMappingAnnotation)) {
            path += postMappingAnnocation.value()[0];
        }
        PutMapping putMappingAnnocation = AnnotationUtils.findAnnotation(method, PutMapping.class);
        if (Objects.nonNull(putMappingAnnocation)) {
            path += postMappingAnnocation.value()[0];
        }
        DeleteMapping deleteMappingAnnotation = AnnotationUtils.findAnnotation(method, DeleteMapping.class);
        if (Objects.nonNull(deleteMappingAnnotation)) {
            path += postMappingAnnocation.value()[0];
        }
        paths.add(path);
        return path;
    }
}
